#!/usr/bin/env bash

# Checker: shellcheck --shell=bash --exclude=SC2006,SC3043 update_gi.sh | less

# Exit on error.
set -e

fatal() {
	echo
	for arg in "$@"; do
		echo "  *  $arg" >&2
	done
	echo

	exit 1
}


# ======== Global constants
DEBUG=${DEBUG:-0}
declare -A UPDATE_URL_MAP
UPDATE_URL_MAP[OS]='https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api/resource?channel_id=1&key=gcStgarh&launcher_id=10'
UPDATE_URL_MAP[CN]='https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/resource?channel_id=1&key=eYd89JmJ&launcher_id=18'
UPDATE_URL_MAP[BB]='https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/resource?channel_id=14&key=KAtdSsoQ&launcher_id=17'
[ "$DEBUG" -eq 1 ] && UPDATE_URL_MAP[OS]='http://127.0.0.1:8000/resource.json'
CONFIG_FILE='config.ini'
ANCHOR_FILE='UnityPlayer.dll'


# ======== Voice pack constants
declare -A LANG_PACKS_PATH_MAP LANG_MAP
LANG_PACKS_PATH_MAP[OS]='GenshinImpact_Data/StreamingAssets/AudioAssets'
LANG_PACKS_PATH_MAP[CN]='YuanShen_Data/StreamingAssets/AudioAssets'
LANG_PACKS_PATH_MAP[BB]=${LANG_PACKS_PATH_MAP[CN]}
LANG_MAP[ENGLISHUS]='en-us'
LANG_MAP[JAPANESE]='ja-jp'
LANG_MAP[KOREAN]='ko-kr'
LANG_MAP[CHINESE]='zh-cn'


# ======== Evaluated variables
THIS_FILE=`basename "$0"`
THIS_PATH=`realpath "$(dirname "$0")"`
REPO_PATH=`dirname "$THIS_PATH"` # parent
reltype=""        # OS, CN or BB. filled later.
remove_archives=1 # 0 or 1. filled later.


# ======== Dependency checks
# Check is all required tools installed.
for appname in jq bash 7za xdelta3; do
	exepath=`command -v "$appname" | tee`
	[ ! -e "$exepath" ] && fatal \
		"Required tool not found!" \
		"Please install: ${appname}."
done


# ======== Download tool setup
DOWNLOAD_PATH="../_update_gi_download"
#DOWNLOAD_PATH="../download with   spaces"
declare -A DL_APP_ARGS_MAP
DL_APP_ARGS_MAP[axel]="-n 15"
DL_APP_ARGS_MAP[aria2c]="--no-conf -c"
DL_APP_ARGS_MAP[fetch]="--force-restart --no-mtime --retry --keep-output --restart"
DL_APP_ARGS_MAP[wget]="-c"
DL_APP_ARGS_MAP[curl]="--disable -O -C -"

# Find first available download tool.
for appname in axel aria2c fetch wget curl; do
	# Pipe to "tee" overwrites the exit status
	exepath=`command -v "$appname" | tee`
	if [ -e "$exepath" ]; then
		DL_APP_BIN="$exepath"
		read -ra DL_APP_ARGS <<< "${DL_APP_ARGS_MAP[$appname]}"
		break
	fi
done
[ ! -e "$DL_APP_BIN" ] && fatal \
	"No downloader application found." \
	"Please install one of: ${DL_APPS_LIST}."
echo "--- Using download application: ${DL_APP_BIN}" "${DL_APP_ARGS[@]}"


# ======== Functions

# MacOS and *BSD do not have md5sum: use md5 instead
exepath=$(command -v md5 | tee)
if [ -e "$exepath" ]; then
	md5check() {
		# 1 = Checksum, 2 = File
		md5 -q -c "$1" "$2" >/dev/null 2>&1
	}
else
	md5check() {
		local input filesum
		# 1 = Checksum, 2 = File
		input=`<<< "$1" tr '[:upper:]' '[:lower:]'`
		filesum=`md5sum "$2" | cut -d ' ' -f1`
		if [ "$input" != "$filesum" ]; then
			echo "Mismatch!"
			exit 1
		fi
	}
fi

download_file() {
	local url="$1"
	local dst_path="$2"
	local md5="$3"
	local filename_args="${dst_path}/${url##*/}"
	local filename="${filename_args%%\?*}"
	local filename_completed="${filename}.completed"

	mkdir -p "$dst_path"
	if [ -f "$filename_completed" ]; then
		echo "    -> Skipping: Already downloaded."
	else
		echo
		(cd "$dst_path" && "$DL_APP_BIN" "${DL_APP_ARGS[@]}" "$url")
		if [ -n "$md5" ]; then
			echo -n "    -> Verifying MD5 checksum ... "
			md5check "$md5" "$filename"
			echo 'OK'
			touch "$filename_completed"
		fi
	fi
}

# Approximate size of the installed archive
download_json_size() {
	local size
	size=`<<< "$1" jq -r -M ".size"`
	echo "~$(((size + 1048576) / 1024 / 1024 / 2)) MiB"
}

# 1: { "path": "URL", "md5": "hash" }
download_json_section() {
	local json_text
	local url md5
	json_text="$1"
	url=`<<< "$json_text" jq -r -M ".path"`
	md5=`<<< "$json_text" jq -r -M ".md5"`
	download_file "$url" "$DOWNLOAD_PATH" "$md5"
}

get_ini_value() {
	local filename variable
	filename="$1"
	variable="$2"
	grep "${variable}=" "${filename}" | tr -d '\r\n' | sed -e 's|.*=||g'
}


# ======== Path sanity checks
# There is a good reason for this check. Do not pollute the game directory.
[ -e "${THIS_PATH}/${ANCHOR_FILE}" ] && fatal \
	"Please move this script outside the game directory prior executing." \
	" -> See README.md for proper installation instructions"
# In case people accidentally want to install the game into the launcher directory.
dllcount=`find ./*.dll 2>/dev/null | wc -l`
[ "$dllcount" -gt 2 ] && fatal \
	"This script is likely run in the wrong directory." \
	"Found more than two DLL files. (expected: 0...2)" \
	"Please run this script in a proper/clean game directory."


# ======== At Exit cleanups
tmp_path="" # json download path
do_remove_config_ini=0 # when the install fails

atexit() {
	if [[ -n "$tmp_path" && -d "$tmp_path" ]]; then
		rm -r "$tmp_path"
	fi

	if [ "$do_remove_config_ini" -gt 0 ]; then
		rm -f "$CONFIG_FILE"
	fi

	echo " -- exit cleanup done -- "
}

trap 'atexit' EXIT


# ======== Command line processing

cli_help=0
cli_install=0
cli_predownload=0
json_path=".game"

for arg in "$@"; do
case "$arg" in
	-h|--help|help)
		cli_help=1
		;;
	install)
		cli_install=1
		;;
	nodelete)
		remove_archives=0
		echo "--- Archives will not be deleted after download"
		;;
	predownload)
		cli_predownload=1
		json_path=".pre_download_game"
		echo "--- Checking for predownload versions"
		;;
	*)
		fatal "Unknown option: ${arg}"
esac
done

if [ "$cli_help" -eq 1 ]; then
	cat << HELP
	${THIS_FILE} [-h|help] [install] [nodelete]

	This script will modify the current working directory.
	See README.md for details and examples.

	"install"     : new game installation from scratch
	"nodelete"    : whether to keep the downloaded archives
	"predownload" : Checks and downloads pre-download-game archives

HELP

	exit 0
fi

# New game installation option
if [ "$cli_install" -eq 1 ]; then
	dircount=`find ./* 2>/dev/null | wc -l`
	if [ "$dircount" -gt 0 ]; then
		fatal "'${PWD}' contains files and/or subdirectories." \
			"Please use an empty directory for a new installation." \
			"To update or resume an installation, rerun this script without the 'install' argument."
	fi
	do_remove_config_ini=1

	echo ""
	echo "    0 -> Genshin Impact [America/Europe/Asia/TW,HK,MO]"
	echo "    1 -> YuanShen       [Mainland China]"
	echo "    2 -> BiliBili       [Mainland China]"
	read -rp "Which version shall be installed? [0]/1/2: " choice

	if [[ -z "$choice" || "$choice" == "0" ]]; then
		reltype="OS"
		echo -ne '[General]\r\nchannel=1\r\ncps=mihoyo\r\ngame_version=0.0.0\r\nsdk_version=\r\nsub_channel=0\r\n' >"$CONFIG_FILE"
	elif [ "$choice" = "1" ]; then
		reltype="CN"
		echo -ne '[General]\r\nchannel=1\r\ncps=mihoyo\r\ngame_version=0.0.0\r\nsdk_version=\r\nsub_channel=1\r\n' >"$CONFIG_FILE"
	elif [ "$choice" = "2" ]; then
		reltype="BB"
		echo -ne '[General]\r\nchannel=14\r\ncps=bilibili\r\ngame_version=0.0.0\r\nsdk_version=\r\nsub_channel=0\r\n' >"$CONFIG_FILE"
	else
		fatal "Invalid selection"
	fi
else
	# Check for existing installation
	if [ -e "GenshinImpact.exe" ]; then
		reltype="OS"
	elif [ -e "YuanShen.exe" ]; then
		if [ -e "$DATADIR/Plugins/PCGameSDK.dll" ]; then
			reltype="BB"
		else
			reltype="CN"
		fi
	fi
fi


# ======== Configuration file parsing
game_not_found_message=(
	"Make sure 'Genshin Impact Game' is the current working directory."
	"If you would like to install the game append the 'install' option:"
	"bash '${THIS_PATH}/${THIS_FILE}' install"
)

[ -z "$reltype" ] && fatal \
	"Cannot determine the installed game type." \
	"${game_not_found_message[@]}"

LANG_PACKS_PATH=${LANG_PACKS_PATH_MAP[$reltype]}
UPDATE_URL=${UPDATE_URL_MAP[$reltype]}
unset reltype

[ ! -e "$CONFIG_FILE" ] && fatal \
	"Game information file ${CONFIG_FILE} not found." \
	"${game_not_found_message[@]}"

installed_ver=`get_ini_value "${CONFIG_FILE}" 'game_version'`
[ -z "$installed_ver" ] && fatal \
	"Cannot read game_version from ${CONFIG_FILE}. File corrupt?"

echo "--- Installed version: ${installed_ver}"


# ======== Update information download + meta checks

# WARNING: File cannot be downloaded to NTFS/FAT* partitions due to special characters
tmp_path=`mktemp -d`
download_file "$UPDATE_URL" "$tmp_path"

RESOURCE_FILE=`find "$tmp_path" -name 'resource*' | tee`

[ ! -f "${RESOURCE_FILE}" ] && fatal \
	"Failed to download version info. Check your internet connection."

upstream_ver=`jq -r -M ".data ${json_path} .latest .version" "$RESOURCE_FILE" | tee`
[ "$upstream_ver" = "null" ] && fatal "Could not find any matching update entry"
echo "--- Latest version: ${upstream_ver}"

if [ "$upstream_ver" = "$installed_ver" ]; then
	echo
	echo "==> Client is up to date."
	exit 0
fi

if [[ "$cli_predownload" -eq 0 && "$DEBUG" -eq 0 ]]; then
	# Check whether this version can be patched
	patcher_dir="$REPO_PATH"/`<<< "$upstream_ver" tr -d .`
	[ ! -d "$patcher_dir" ] && fatal \
		"No suitable patch script found. Please check the bug tracker for details about the progress."
fi

# ======== Select update type

# Check is diff update possible.
archive_json=`jq -r -M ".data ${json_path} .diffs[] | select(.version==\"${installed_ver}\")" "$RESOURCE_FILE"`
if [ -z "$archive_json" ]; then
	# Fallback to full download.
	archive_json=`jq -r -M ".data ${json_path} .latest" "$RESOURCE_FILE"`
	dl_type="new installation"

	[ "$cli_install" -eq 0 ] && fatal "Cannot find an update for ${installed_ver} -> ${upstream_ver}." \
		"Please use a new directory to install the game from scatch."
else
	dl_type="incremental update"
fi
size=`download_json_size "$archive_json"`
echo "Download type: ${dl_type} (${size})"

if [ "$cli_install" -eq 1 ]; then
	updated_ver="$upstream_ver"
else
	# The version after updating might differ from the newest available version (bad package selection?)
	updated_ver=$(<<< "$archive_json" jq -r -M ".name")
	[[ "$updated_ver" =~ \.[0-9]+\.[0-9]+_([0-9]+\.[0-9]+\.[0-9]+)_ ]];
	updated_ver=${BASH_REMATCH[1]}
	if [ "$upstream_ver" != "$updated_ver" ]; then
		echo -e "=!= WARNING: Cannot find a direct update for ${installed_ver} -> ${upstream_ver}.\n" \
			"   This script will first update to version ${updated_ver}.\n" \
			"   Please re-run the update script after completing this update."
	fi
fi


# Confirm install/update.
while :; do
	if [ "$DEBUG" -eq 0 ]; then
		read -rp "Start/continue update? [Y]/n: " input
	else
		input="y"
	fi
	case "$input" in
		Y|y|'')
			echo
			break
			;;
		n|N)
			exit 0
			;;
	esac
done

echo "--- Main game archive"

# Download SDK if exists
sdk_json=$(jq -r -M ".data .sdk | select(.!=null)" "$RESOURCE_FILE")
if [ -n "$sdk_json" ]; then
	download_json_section "$sdk_json"
fi

# Get all segments (array) or empty string
main_archive_segments=$(<<< "$archive_json" jq -r -M ".segments | select(.!=null)")

# Download the main game or update archive(s)
if [ -n "$main_archive_segments" ]; then
	i=0
	while :; do
		section=$(<<< "$main_archive_segments" jq -r -M ".[${i}] | select(.!=null)")
		[ -z "$section" ] && break
		download_json_section "$section"
		i=$((i + 1))
	done
else
	download_json_section "$archive_json"
fi


# ======== Locate and install voiceover packs

lang_dir_names() {
	if [ -d "$LANG_PACKS_PATH" ]; then
		# Get voiceover directory name in capitals. Does proper space handling.
		find "$LANG_PACKS_PATH" -mindepth 1 -type d -print0 \
			| xargs -0 -L1 -r basename \
			| tr -d '()' \
			| tr '[:lower:]' '[:upper:]'
	fi
}
lang_dir_names_str=$(lang_dir_names)
if [[ -z "$lang_dir_names_str" && "$cli_install" -eq 1 ]]; then
	# TODO: make the default language(s) selectable
	lang_dir_names_str="ENGLISHUS"
fi

# Download langs packs.

while read -r dir_name; do
	[ -z "$dir_name" ] && continue
	lang_code=${LANG_MAP[$dir_name]}

	lang_archive_json=`<<< "${archive_json}" jq -r -M ".voice_packs[] | select(.language==\"${lang_code}\")"`
	if [ "$lang_archive_json" = 'null' ] || [ "$lang_archive_json" = '' ]; then
		echo "--- Cannot find update for language: ${dir_name}"
		continue
	fi
	size=`download_json_size "$lang_archive_json"`
	echo "--- Voiceover pack: ${lang_code} (${size})"
	download_json_section "$lang_archive_json"
done <<< "$lang_dir_names_str"


# ======== Revert patch & apply update

if [ "$cli_predownload" -eq 1 ]; then
	echo
	echo "==> Pre-download completed. The archives will be ready on release day."
	exit 0
fi

# Run 'patch_revert.sh' on update existing installation.
if [ -e "$ANCHOR_FILE" ]; then
	echo
	echo "============== Reverting previous Wine patch ==============="
	[ "$DEBUG" -eq 0 ] && bash "${patcher_dir}/patch_revert.sh"
	echo "============================================================"
	echo
fi


# Unpack the game files and remove old ones according to deletefiles.txt
echo "--- Updating game files ..."

# -L to allow symlinking the download directory to another drive
zip_files=$(find -L "$DOWNLOAD_PATH" \( -name "*.zip" -o -name "*.zip.001" \))
while read -r archive_name; do
	[ ! -f "${archive_name}.completed" ] && fatal \
		"Archive '${archive_name}' is not marked as complete!"

	bash "${THIS_PATH}/perform_update.sh" "$archive_name"
done <<< "$zip_files"

if [ "$remove_archives" -eq 1 ]; then
	# Remove downloads when all updates succeeded
	# otherwise keep the archives to fix with "perform_update.sh" manually
	echo "--- Removing downloaded archives ..."
	while read -r archive_name; do
		rm -f "${archive_name}" "${archive_name}.completed"
	done <<< "$zip_files"
fi


# ======== Config update and game patching
do_remove_config_ini=0

# Update version in config file.
sed -i "s/game_version=${installed_ver}/game_version=${updated_ver}/" "$CONFIG_FILE"

if [ -n "$sdk_json" ]; then
	sdk_version=`<<< "$sdk_json" jq -r -M ".version"`
	sed -i "s/^sdk_version=.*/sdk_version=${sdk_version}/" "$CONFIG_FILE"
fi

if [ "$remove_archives" -eq 1 ]; then
	# The directory should be empty now. Remove.
	rm -r "$DOWNLOAD_PATH"
fi
echo
echo "==> Update to version ${updated_ver} completed"


# Run wine compatibility patch script.
echo
echo "================= Applying new Wine patch =================="
[ "$DEBUG" -eq 0 ] && bash "${patcher_dir}/patch.sh"
echo "============================================================"
echo "==> Update script completed successfully"


exit 0
